Array of Imports with Monikers

There are variety of import patterns to declare imports in box::use(). The specific pattern: mod[foo, bar, ...] allows specific qualification of imports, similar to from mod import foo, bar in Python, which uses indexing mechanism in R. Let’s first compare the traditional R approach to load/attach imports against {box}. Here’s the following table:

Aspect Base R {box} Approach
Loading Method library(dplyr) box::use(dplyr) / box::use(dplyr[...])
Namespace Impact Attaches to global namespace Module-scoped imports
Conflict Handling May cause naming conflicts Explicit imports prevent conflicts
Code Clarity Dependencies implicit Dependencies explicitly declared
Performance Loads entire package namespace More controlled

Oh, and there’s also require() function, an “evil-ler” twin of library() function, where it won’t just attach the entire namespace to the search path if the package exist, it returns in Boolean (TRUE if the package exists; FALSE if it doesn’t) when executed. There’s an inconsistency: it fails silently (“fails silently” means that a function fails without stopping the program or showing a clear error message).

Importing R Packages and scripts / folders

If the table is not enough to you, this section will further explain. Let’s explore the different importing methods:

Basic Imports

Chapter 1 already shows you the basic ways to import modules using box::use(). This function allows multiple imports at once, whether that module is a package, a local module, or packages managed by {carrier}. This is not different from the ellipsis (...) that allows passing multiple arguments, similar to Python’s *args and **kwargs. Differently, there’s a feature in box::use()’s bracketing method mod[] called “wildcard” in the form of ..., e.g. mod[...], which will attach all the names under that namespace.

Since this allows you to import multiple packages and their functions in a single call, separated by comma, you don’t have to bother yourself calling multiple calls, like:

box::use(
    # Packages
    dplyr, ggplot2, stats,
    dplyr[filter, select, mutate], 
    ggplot2[ggplot, geom_point], 
    stats[lm], 
    
    # Local modules
    ./mod, ./folder/mod, 
    ./mod[fn1, ...], 
    ./folder/mod[fn1, ...], 
    
    # Carrier packages
    mod, mod/submodule, 
    mod[fn1, ...], 
    mod/submodule[fn1, ...]
)

When importing scripts and modules, you are going to provide the name of the path (should be a literal name, not a string), and/or add prefix ./ that indicates the current path. The use of ../ is allowed as well, but this will be discussed in Chapter 3.4. The rules applied on {box}-{carrier} modules except you are not allowed to place a prefix (./ or ../) since they are interpreted as packages, not local modules.

Importing the entirety

Do not confuse this with library(), where it attaches the entire namespace to the search path. Throwing all the exports into the search path is often subjectively (or objectively?) bad and is being discouraged in best software engineering practices. {box} has a different: This is the same as Python’s import math semantics, where instead of loading the package goes to the global environment, the namespace of the package and scripts / folders, including the functions and other objects, such as data frames or constants like pi from base R, are encapsulated as environments, (another data structure, similar to lists but mutable) with its name, and then the imports are accessed directly with $ subset operator.

box::use(
    dplyr,
    ./mod1, 
    ./mod2
)  

dplyr$select(data, col1, col2)
mod1$fn1(mod2$fn3(5))

How beautiful this may be? The entire package namespace becomes available as an environment. All functions are (and must) be accessed using the package name as a prefix and $ operator. This is nice as it prevents namespace pollution while maintaining access to all functions. This might not sound useful since you already did that with pkg:: in base R, this is still useful, especially for local modules and {box}-{carrier} modules, where you don’t want to allow to qualify their names.

The use of aliases also allowed:

box::use(
    dpr = dplyr, 
    tdr = tidyr, 
    md1 = ./module1,
    md2 = ./module2
)  

# Usage:
dpr$filter(data, col1 > 0) |> 
    tdr$pivot_longer(cols = col2)

md1$fn1(md2$fn3(5))

Granular Imports with Aliases {spec-imports}

{box} absolves namespace clashes, which commonly occur when different packages have functions with the same name.

As you import the names, you are allowed to spice up things a little bit by renaming the original names during the imports. This is superbly useful when you have 2 packages to be used and you want to use them both at once. For instance, the {dplyr}‘s filter() function, and as you attach the {dplyr} namespace, it will mask the existing functions from the global namespace, namely the {stats}filter() function. This induces namespace clash, and there are times you may not want this happening.

Fortunately with {box}, you can load the filter() function from the {dplyr} namespace through the following:

box::use(
    dplyr[keep_when = filter],  # dplyr's `filter()` becomes `keep_when()`
    stats[filter_ts = filter]   # stats' filter becomes filter_ts
)

data |> keep_when(col > 0)      
AirPassengers |> filter_ts()      

But wait, there’s another thing you need to know. There’s a gotcha when you declare an import under box::use(), i.e. something like box::use(pkg = package[fn1]), two things will happen: this imports pkg carrying the entire package namespace, and at the same time will attach fn1 names under package namespace.

Note

Granular imports in R’s library() also allowed using include.only parameter.

For example:

library(dplyr, include.only = c("select", "filter"))

But still no alias gimmicks and doesn’t even leverage non-standard evaluation, where it treats include.only arguments as an object, called as a name, unlike box::use().

R version 4.4 and above has a shorthand of library(pkg, include.only = c('fn1', 'fn2')): Introducing the base::use().

Example usage:

use(dplyr, c("select", "filter"))

This is inconsistent, as well, and generally not recommendable..

Importing non-syntactic names

The infix operators like %>% from {magrittr} are considered non-syntactic names. In R, operators and functions that use special characters (like +, *, %>%, %in%, %*%, etc.) are called infix operators but internally parsed as the usual function call in R, e.g. `+`(a, b). These operators require special handling when importing with {box}. Regardless, yes, these are still functions.

To import functions with special characters, you need to wrap them in backticks:

box::use(
    magrittr[`%>%`, `%T>%`], 
    dplyr[group_by, summarise, n, mutate],
    stats[sd, median]
)

iris %>% 
    group_by(Species) %>% 
    summarise(
        mu = mean(Sepal.Length, na.rm = TRUE), 
        sigma = sd(Sepal.Length, na.rm = TRUE),
        md = median(Sepal.Length, na.rm = TRUE)
    ) %T>%
    print() %>% 
    mutate(cv = sigma / mu, .before = md)
# A tibble: 3 × 4
  Species       mu sigma    md
  <fct>      <dbl> <dbl> <dbl>
1 setosa      5.01 0.352   5  
2 versicolor  5.94 0.516   5.9
3 virginica   6.59 0.636   6.5
# A tibble: 3 × 5
  Species       mu sigma     cv    md
  <fct>      <dbl> <dbl>  <dbl> <dbl>
1 setosa      5.01 0.352 0.0704   5  
2 versicolor  5.94 0.516 0.0870   5.9
3 virginica   6.59 0.636 0.0965   6.5

Wildcard import

The true equivalent of library(pkg) is through box::use(pkg[...]). Yet again, ... are called “ellipsis”. The use of ... sets as a “wildcard”, and this, while being granular, imports all the namespace within the package (or modules). The Python’s equivalent would be from pkg import *.

For example:

box::use(
    dplyr[...]
)

Imports within the function / function call

When we import packages / scripts / folders as modules or import their namespace, did you know the imports are enclosed within the scope?

According to the official documentation:

the effects of box::use() are restricted to the current scope: we can load and attach names inside a function, and this will not affect the calling scope (or elsewhere).

Here’s an example:

Code
mtcars |> 
    dplyr::reframe(
        {    
            box::use(
                stats[linear_reg = lm, pearson_r = cor],
                purrr[imap_dfc, set_names],
                tibble[tbl = tibble]
            ) 
            
            model = linear_reg(mpg ~ wt)
            coefs = coef(model)
            coef_table = imap_dfc(coefs, \(bi, nm) {
                result = tbl(bi)
                set_names(result, nm)
            })
            
            corr = pearson_r(wt, mpg)
            
            test = summary(model)
            tbl(
                coef_table, 
                corr = corr, 
                rsq = test$r.squared,
                adj_rsq = test$adj.r.squared
            )
        },
        
        .by = cyl
    )
#>   cyl (Intercept)        wt       corr       rsq   adj_rsq
#> 1   6    28.40884 -2.780106 -0.6815498 0.4645102 0.3574122
#> 2   4    39.57120 -5.647025 -0.7131848 0.5086326 0.4540362
#> 3   8    23.86803 -2.192438 -0.6503580 0.4229655 0.3748793

This code is made to study the type-I error by examining how true the linear relationship between wt and mpg variables from mtcars data, when performing statistical analysis. Here, this code imports within dplyr::reframe() function call without making side-effect the current environment. This is great if you create a function with external dependencies available from R packages, or within your scripts / folders.

Best Practices for Package Imports

But, of course, R packages have strengths, but don’t forget their flaws. I will enumerate the do’s when using {box} package:

  1. Be Specific with Imports

    Please, do not import everything, unless there’s a good use case, like when you ALL of them. The use of wildcards, i.e. the “ellipsis” ... within the indexing syntax through [...] is a shortcut to import the namespace, but you are importing everything here.

    box::use(
        dplyr[...]
    )

    Don’t just do this in actual practice, or it will create a mess in the global namespace, just like library(). As the Zen of Python said: “Explicit is better than implicit.”

    Instead, import only what you need. Of course, in several times, you only import specific parts of the package only. For instance, when you are aggregating data frame with {dplyr}, you often only needs filter(), select(), mutate(), group_by(), and summarise(). Mind you that there are a total of 293 exported namespaces (will be less than that if you don’t count the pseudo-functions, such as across() and where()) within {dplyr} package, and for your aggregation task, you only need 5 out of the total exports.

    This approach is better because it is explicit and you can even rename those imports:

    box::use(
        dplyr[filter_df = filter, select, mutate, group_by, summarise]
    )

    Let’s take an example, where you want to calculate the sample size, mean, standard deviation, standard error, and the coefficient of variation across the numeric columns in iris dataset:

    box::use(
        dplyr[n, mutate, relocate, group_by, summarise, everything], 
        tidyr[melt = pivot_longer, spread = pivot_wider]
    )
    
    iris |> 
        summarise(
            across(
                where(is.numeric), 
                list(
                    mu = \(x) mean(x, na.rm = TRUE), 
                    sigma = \(x) sd(x, na.rm = TRUE)
                ), 
                .names = "{.col}..{.fn}"
            ), 
            n = n()
        ) |> 
        melt(
            cols = everything() & !n,
            names_pattern = "^(.*)\\.\\.(.*)$",
            names_to = c("Variable", "Statistics"),
            values_to = "Est"
        ) |> 
        spread(
            names_from = Statistics, 
            values_from = Est
        ) |> 
        mutate(
            se = sigma / sqrt(n), 
            cv = sigma / mu
        ) |> 
        relocate(
            n, .after =  Variable
        )
    # A tibble: 4 × 6
      Variable         n    mu sigma     se    cv
      <chr>        <int> <dbl> <dbl>  <dbl> <dbl>
    1 Sepal.Length   150  5.84 0.828 0.0676 0.142
    2 Sepal.Width    150  3.06 0.436 0.0356 0.143
    3 Petal.Length   150  3.76 1.77  0.144  0.470
    4 Petal.Width    150  1.20 0.762 0.0622 0.636
  2. Group Related Imports

    This is just an opinion. For better clarity, you can explain the imports you are making within the function call box::use() with comments #. This is not unusual, sometimes this is common. Since comments are allowed, you can group the imports by functionality.

    box::use(
        # Simple aggregation
        dplyr[filter_df = filter, select, mutate, group_by, summarise],
        tidyr[pivot_longer, pivot_wider],
    
        # To run linear regression and t-test
        stats[linear_reg = lm, welch_ttest = t.test],
    
        # To visualize outputs from linear regression and t-test
        ggplot2[ggplot, geom_point, geom_smooth, geom_box, theme_minimal, aes]
    )
  3. Handle Naming Conflicts

    As discussed here, you are allowed to place an alias within the imports, so that the namespace clash will be resolved.

    The most prominent example is dplyr::filter() and stats::filter().

    box::use(
        dplyr[keep_when = filter],
        stats[filter_ts = filter]
    )

    Rename them whatever you want to employ clarity.

Troubleshooting Package Imports

Frankly, I will show you solutions if you have similar errors like the following. The common issues to be found when importing packages happen are:

  1. Obviously, when the R package is not installed. This matter is trivial: The package does not exist in your current environment, and simply just install the packages you need and import them using box::use().

    box::use(pkg[func, ...])
    #> Error in box::use(pkg) : there is no package called ‘pkg’
    
    install.packages("pkg")
  2. When the particular imports does not exist in the package namespace or incorrectly name the import:

    box::use(dplyr[nonexistent_function])
    #> Error in box::use(dplyr[nonexistent_function, slct]) : 
         name “nonexistent_function”, “slct” not exported by “dplyr”
    
    box::use(dplyr[select, filter])

    {box} enforces strict naming. If possible, check out the official documentation of the R package. Check if you are using indeed correct spelled name, and check if the imports does exist in the package namespace